/*	
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkFile, ts } from "arkanalyzer";
import { AdviceChecker, BaseMetaData } from "../BaseChecker";
import { Rule, Defects, AstMatcher, MatcherTypes, MatcherCallback } from '../../Index';
import { RuleListUtil } from "../../utils/common/DefectsList";
import { IssueReport } from "../../model/Defects";

interface RuleOption {
    includeExports?: boolean;
}

const NAMED_TYPES: string[] = ["ImportSpecifier", "ExportSpecifier"];
const NAMESPACE_TYPES: string[] = [
    "ImportNamespaceSpecifier",
    "ExportNamespaceSpecifier",
];
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/no-duplicate-imports.md',
    description: '"module" import is duplicated.'
};

function isImportExportSpecifier(importExportType: string, type: 'named' | 'namespace'): boolean {
    return (type === 'named' ? NAMED_TYPES : NAMESPACE_TYPES).includes(importExportType);
}

function getImportExportType(node: ts.Node): string {
    if (ts.isImportDeclaration(node)) {
        const namedBindings = node.importClause?.namedBindings;
        if (namedBindings) {
            return ts.isNamespaceImport(namedBindings) ? 'ImportNamespaceSpecifier' : 'ImportSpecifier';
        }
        return node.importClause?.name ? 'ImportDefaultSpecifier' : 'SideEffectImport';
    }
    if (ts.isExportDeclaration(node)) {
        if (node.exportClause) {
            return ts.isNamespaceExport(node.exportClause) ? 'ExportNamespaceSpecifier' : 'ExportSpecifier';
        }
        return 'ExportAll';
    }
    return 'SideEffectImport';
}

function hasDefaultImport(node: ts.Node): boolean {
    return ts.isImportDeclaration(node) && !!node.importClause?.name;
}

function hasNamedImports(node: ts.Node): boolean {
    return ts.isImportDeclaration(node) && ts.isNamedImports(node.importClause?.namedBindings || ({} as any));
}

function hasInlineTypeModifier(node: ts.Node): boolean {
    if (ts.isImportDeclaration(node) && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
        return node.importClause.namedBindings.elements.some(element => element.isTypeOnly);
    }
    return false;
}

function isComplementaryTypeImports(node1: ts.Node, node2: ts.Node): boolean {
    if (!ts.isImportDeclaration(node1) || !ts.isImportDeclaration(node2)) {
        return false;
    }
    const node1IsTypeOnly = node1.importClause?.isTypeOnly === true;
    const node2IsTypeOnly = node2.importClause?.isTypeOnly === true;
    const node1HasInlineType = hasInlineTypeModifier(node1);
    const node2HasInlineType = hasInlineTypeModifier(node2);
    return (node1IsTypeOnly && !node1HasInlineType && !node2IsTypeOnly && node2HasInlineType) ||
           (node2IsTypeOnly && !node2HasInlineType && !node1IsTypeOnly && node1HasInlineType);
}

function isImportExportCanBeMerged(nodeOne: ts.Node, nodeTwo: ts.Node): boolean {
    const typeOne = getImportExportType(nodeOne);
    const typeTwo = getImportExportType(nodeTwo);

    const isExportAllConflict =
        (typeOne === 'ExportAll' && typeTwo !== 'ExportAll' && typeTwo !== 'SideEffectImport') ||
        (typeOne !== 'ExportAll' && typeOne !== 'SideEffectImport' && typeTwo === 'ExportAll');
    if (isExportAllConflict) {
        return false;
    }
    const isNamespaceNamedConflict =
        (isImportExportSpecifier(typeOne, 'namespace') && isImportExportSpecifier(typeTwo, 'named')) ||
        (isImportExportSpecifier(typeTwo, 'namespace') && isImportExportSpecifier(typeOne, 'named'));
    if (isNamespaceNamedConflict) {
        return false;
    }
    if (ts.isImportDeclaration(nodeOne) && ts.isImportDeclaration(nodeTwo)) {
        const bothTypeOnly = nodeOne.importClause?.isTypeOnly && nodeTwo.importClause?.isTypeOnly;
        if (bothTypeOnly && ((hasDefaultImport(nodeOne) && hasNamedImports(nodeTwo)) ||
                             (hasDefaultImport(nodeTwo) && hasNamedImports(nodeOne)))) {
            return false;
        }
        if (isComplementaryTypeImports(nodeOne, nodeTwo)) {
            return false;
        }
    }
    return true;
}

function shouldReportImportExportDeclarations(node: ts.Node, previousNodes: ts.Node[]): boolean {
    return previousNodes.some(prevNode => isImportExportCanBeMerged(node, prevNode));
}

function filterNodesByDeclarationType(nodeEntries: Array<{ node: ts.Node, declarationType: string }>, targetType: string): ts.Node[] {
    return nodeEntries.filter(({ declarationType }) => declarationType === targetType).map(({ node }) => node);
}

function getModule(node: ts.Node): string {
    if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
        const moduleSpecifier = node.moduleSpecifier;
        if (moduleSpecifier && ts.isStringLiteral(moduleSpecifier)) {
            return moduleSpecifier.text.trim();
        }
    }
    return '';
}

function isTypeImport(node: ts.Node): boolean {
    return ts.isImportDeclaration(node) && node.importClause?.isTypeOnly === true;
}

export class NoDuplicateImportsCheck implements AdviceChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public issues: IssueReport[] = [];
    private modules: Map<string, Array<{ node: ts.Node, declarationType: string }>> = new Map();
    private includeExports: boolean = false;
    private currentFile: ArkFile | null = null;

    private importExportMatcher: AstMatcher = {
        matcherType: MatcherTypes.AST,
        match: (node: ts.Node) => ts.isImportDeclaration(node) || ts.isExportDeclaration(node)
    };

    public registerMatchers(): MatcherCallback[] {
        return [{ matcher: this.importExportMatcher, callback: this.check }];
    }

    public beforeCheck(): void {
        const ruleOption = this.rule.option?.[0] as RuleOption | undefined;
        this.includeExports = ruleOption?.includeExports !== undefined ? ruleOption.includeExports : false;
        this.modules.clear();
    }

    public afterCheck(): void {}

    public check = (node: ts.Node, file: ArkFile): void => {
        this.currentFile = file;
        if (ts.isImportDeclaration(node)) {
            this.handleImportExports(node, 'import');
        } else if (this.includeExports && ts.isExportDeclaration(node)) {
            this.handleImportExports(node, 'export');
        }
    };

    private handleImportExports(node: ts.Node, declarationType: string): void {
        const module = getModule(node);
        if (!module) {
            return;
        }
        this.checkAndReport(node, declarationType);
        const currentNode = { node, declarationType };
        const previousNodes = this.modules.get(module) || [];
        this.modules.set(module, [...previousNodes, currentNode]);
    }

    private checkAndReport(node: ts.Node, declarationType: string): void {
        const module = getModule(node);
        if (!this.modules.has(module)) {
            return;
        }
        const previousNodes = this.modules.get(module) || [];
        const importNodes = filterNodesByDeclarationType(previousNodes, 'import');
        const exportNodes = this.includeExports ? filterNodesByDeclarationType(previousNodes, 'export') : [];

        const messages: string[] = [];
        if (declarationType === 'import') {
            if (shouldReportImportExportDeclarations(node, importNodes)) {
                const isType = isTypeImport(node);
                messages.push(isType ? `${module} type import is duplicated.` : `'${module}' import is duplicated.`);
            }
            if (this.includeExports && shouldReportImportExportDeclarations(node, exportNodes)) {
                const isType = isTypeImport(node);
                messages.push(isType ? `${module} type import is duplicated as export.` : `'${module}' import is duplicated as export.`);
            }
        } else if (declarationType === 'export') {
            if (shouldReportImportExportDeclarations(node, exportNodes)) {
                messages.push(`'${module}' export is duplicated.`);
            }
            if (shouldReportImportExportDeclarations(node, importNodes)) {
                messages.push(`'${module}' export is duplicated as import.`);
            }
        }

        const severity = this.rule.alert ?? this.metaData.severity;
        messages.forEach(message => this.addIssueReport(node, message, severity));
    }
    private addIssueReport(node: ts.Node, description: string, severity: number): void {
        const sourceFile = node.getSourceFile();
        const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
        const endCharacter = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).character;

        const defect = new Defects(
            line + 1,
            character + 1,
            endCharacter + 1,
            description,
            severity,
            this.rule.ruleId,
            this.currentFile?.getFilePath() || '',
            this.metaData.ruleDocPath,
            true,
            false,
            false,
            false
        );
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
};
